/*
 * Copyright (c) 1990, 2022, shwfed and/or its affiliates. All rights reserved.
 *
 * @date: 2023-03-09
 *
 */
package cn.junglead.common.utils;

import lombok.extern.slf4j.Slf4j;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;

/**
 * 哈希加盐的密码算法工具。密码学来源：
 * <a href="https://crackstation.net/hashing-security.htm">...</a>
 *
 * @author Gary.Wang
 * @version 1.0
 */
@Slf4j
public class HashSaltPasswordUtil {

	private HashSaltPasswordUtil(){}
    private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";

    // 可以在不破坏现有哈希的情况下更改以下常量
    /**
     * 加盐的位数
     */
    public static final int SALT_BYTE_SIZE = 24;
    /**
     * 哈希的长度位数
     */
    public static final int HASH_BYTE_SIZE = 24;
    /**
     * 迭代次数
     */
    public static final int PBKDF2_ITERATIONS = 1000;
    /**
     * 迭代索引
     */
    public static final int ITERATION_INDEX = 0;
    /**
     * 加盐索引
     */
    public static final int SALT_INDEX = 1;
    /**
     * 算法索引
     */
    public static final int PBKDF2_INDEX = 2;

    /**
     *
     * 返回密码的加盐PBKDF2哈希。
     *
     * @param plainText 密码明文
     *
     * @return 返回加密后的密文
     */
    public static String createHash(String plainText) {
        return createHash(plainText.toCharArray());
    }

    /**
     *
     * 返回密码的加盐PBKDF2哈希。
     *
     * @param plainText 密码明文
     *
     * @return 返回加密后的密文
     */
    public static String createHash(char[] plainText) {
        try{
			// Generate a random salt
			SecureRandom random = new SecureRandom();
			byte[] salt = new byte[SALT_BYTE_SIZE];
			random.nextBytes(salt);
			// Hash the plainText
			byte[] hash = pbkdf2(plainText, salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
			// format iterations:salt:hash
			return PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" + toHex(hash);
		} catch (Exception exception){
			log.error("哈希加盐计算密码发生异常，异常信息：{}", exception.getMessage(), exception);
		}
		return null;
    }

    /**
     *使用哈希验证密码。
     *
     *@param plainText 要检查的密码
     *@param correctHash 有效密码的哈希
     *@return 如果密码正确则返回true，否则返回false
     */
    public static boolean validatePassword(String plainText, String correctHash) {
		return validatePassword(plainText.toCharArray(), correctHash);
    }

    /**
     *
     * 使用哈希验证密码。
     *
     * @param plainText 要检查的密码
     * @param correctHash 有效密码的哈希
     *
     * @return 如果密码正确则返回true，否则返回false
     */
    public static boolean validatePassword(char[] plainText, String correctHash) {
        try{
			// Decode the hash into its parameters
			String[] params = correctHash.split(":");
			int iterations = Integer.parseInt(params[ITERATION_INDEX]);
			byte[] salt = fromHex(params[SALT_INDEX]);
			byte[] hash = fromHex(params[PBKDF2_INDEX]);
			// Compute the hash of the provided plainText, using the same salt,
			// iteration count, and hash length
			byte[] testHash = pbkdf2(plainText, salt, iterations, hash.length);
			// Compare the hashes in constant time. The plainText is correct if
			// both hashes match.
			return slowEquals(hash, testHash);
		} catch (Exception exception){
			log.error("哈希加盐验证密码发生异常，异常信息：{}", exception.getMessage(), exception);
		}
		return false;
    }

    /**
     *在长度恒定的时间内比较两个字节数组。这种比较法
     *用于使密码哈希无法从联机提取
     *系统采用定时攻击，然后离线攻击。
     *
     *@param a 第一个字节数组
     *@param b 第二个字节数组
     *@return true如果两个字节数组相同，则返回false
     */
    private static boolean slowEquals(byte[] a, byte[] b) {
        int diff = a.length ^ b.length;
        for (int i = 0; i < a.length && i < b.length; i++) {
            diff |= a[i] ^ b[i];
        }
        return diff == 0;
    }

    /**
     * 计算密码的PBKDF2散列。
     * @param password 哈希的密码
     * @param salt 盐
     * @param iterations 迭代次数（慢度因子）
     * @param bytes 要计算的哈希长度（以字节为单位）
     * @return 密码的PBKDF2哈希
     * @throws NoSuchAlgorithmException 不存在的算法异常
     * @throws InvalidKeySpecException 错误的密钥异常
     */
    private static byte[] pbkdf2(char[] password, byte[] salt, int iterations, int bytes) throws NoSuchAlgorithmException, InvalidKeySpecException {
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
        SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
        return skf.generateSecret(spec).getEncoded();
    }

    /**
     *将十六进制字符字符串转换为字节数组。
     *
     *@param hex 十六进制字符串
     *@return 解码为字节数组的十六进制字符串
     */
    private static byte[] fromHex(String hex) {
        byte[] binary = new byte[hex.length() / 2];
        for (int i = 0; i < binary.length; i++) {
            binary[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
        }
        return binary;
    }

    /**
     *将字节数组转换为十六进制字符串。
     *
     *@param array 要转换的字节数组
     *@return a length*2字符串编码字节数组
     */
    private static String toHex(byte[] array) {
        BigInteger bi = new BigInteger(1, array);
        String hex = bi.toString(16);
        int paddingLength = (array.length * 2) - hex.length();
        if (paddingLength > 0) {
            return String.format("%0" + paddingLength + "d", 0) + hex;
        } else {
            return hex;
        }
    }
}
